﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace RanbuOM.Serialization
{
    public class BitTracker
    {
        public readonly SortedDictionary<long, long> Coverages;

        public long Length { get; private set; }

        public BitTracker(long length)
        {
            this.Length = length;
            this.Coverages = new SortedDictionary<long, long>();
        }

        public long CoveredByteCount()
        {
            return this.Coverages.Sum(c => (c.Value - c.Key));
        }

        public long MissingByteCount()
        {
            return this.Length - this.CoveredByteCount();
        }

        public void AddCoverage(long begin, int count)
        {
            long end = begin + count;
            if (end > Length + 1)
            {
                throw new ArgumentException("begin + count > length + 1");
            }

            var query = from c in this.Coverages
                           where begin <= c.Value && end >= c.Key
                           select c;
            var overlaps = query.ToArray();

            if (overlaps.Length > 0)
            {
                foreach (var overlap in overlaps)
                {
                    this.Coverages.Remove(overlap.Key);
                }

                begin = Math.Min(begin, overlaps.Min(o => o.Key));
                end = Math.Max(end, overlaps.Max(o => o.Value));
            }

            this.Coverages.Add(begin, end);
        }
    }

    public class BinaryReaderEx : IDisposable
    {
        private readonly BinaryReader reader;

        public Endianness Endianness { get; private set; }

        public BitTracker Tracker { get; private set; }

        public BinaryReaderEx(Stream stream, Encoding encoding = null, Endianness endianness = Endianness.LittleEndian, bool tracking = false)
        {
            if (encoding == null)
            {
                encoding = Encoding.Default;
            }

            this.reader = new BinaryReader(stream, encoding);
            this.Endianness = endianness;
            if (tracking)
            {
                this.Tracker = new BitTracker(this.Length);
            }
        }

        private void Track(long start, int count)
        {
            if (this.Tracker != null)
            {
                this.Tracker.AddCoverage(start, count);
            }
        }

        private void Track(int count)
        {
            this.Track(this.Position, count);
        }

        public byte ReadU8()
        {
            this.Track(1);
            return this.reader.ReadByte();
        }

        public sbyte ReadI8()
        {
            this.Track(2);
            return this.reader.ReadSByte();
        }

        public ushort ReadU16()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(2);
            return BitConverter.ToUInt16(bytes, 0);
        }

        public short ReadI16()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(2);
            return BitConverter.ToInt16(bytes, 0);
        }

        public uint ReadU32()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(4);
            return BitConverter.ToUInt32(bytes, 0);
        }

        public int ReadI32()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(4);
            return BitConverter.ToInt32(bytes, 0);
        }

        public ulong ReadU64()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(8);
            return BitConverter.ToUInt64(bytes, 0);
        }

        public long ReadI64()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(8);
            return BitConverter.ToInt64(bytes, 0);
        }

        public float ReadR4()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(4);
            return BitConverter.ToSingle(bytes, 0);
        }

        public double ReadR8()
        {
            var bytes = this.ReadEndiannessEffectiveBytes(8);
            return BitConverter.ToDouble(bytes, 0);
        }

        public string ReadCString()
        {
            long start = this.Position;
            int count = 1;
            StringBuilder sb = new StringBuilder();
            for (char c = this.reader.ReadChar(); c != '\0'; c = this.reader.ReadChar())
            {
                sb.Append(c);
                count++;
            }

            this.Track(start, count);
            return sb.ToString();
        }

        public string ReadCharArrayString(int count)
        {
            this.Track(count);
            var chars = this.reader.ReadChars(count);
            return new string(chars);
        }

        public char ReadChar()
        {
            this.Track(1);
            return this.reader.ReadChar();
        }

        public byte[] ReadBytes(int count)
        {
            this.Track(count);
            return this.reader.ReadBytes(count);
        }

        public void Seek(long offset, SeekOrigin seekOrigin = SeekOrigin.Begin)
        {
            this.reader.BaseStream.Seek(offset, seekOrigin);
        }

        public long Position
        {
            get { return this.reader.BaseStream.Position; }
        }

        public long Length
        {
            get { return this.reader.BaseStream.Length; }
        }

        public void Close()
        {
            this.reader.Close();
        }

        public void Dispose()
        {
            this.reader.Dispose();
        }

        private byte[] ReadEndiannessEffectiveBytes(int count)
        {
            this.Track(count);
            var bytes = this.reader.ReadBytes(count);
            if (this.Endianness != Endianness.LittleEndian)
            {
                Array.Reverse(bytes);
            }

            return bytes;
        }
    }
}
